package com.swak.persistence.mybatis.filter;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Invocation;
import org.springframework.beans.BeanUtils;

import com.swak.asm.FieldCache.FieldMeta;
import com.swak.define.TableDefine;
import com.swak.define.TableDefines;
import com.swak.define.TableParser;
import com.swak.persistence.QueryCondition;
import com.swak.sharding.ShardingParam;
import com.swak.sharding.ShardingStrategy;
import com.swak.sharding.ShardingStrategys;
import com.swak.utils.Maps;
import com.swak.utils.StringUtils;

/**
 * 分表插件: 只支持最简单的模式 - 单表模式， 如果需要支持复杂的模式可以使用 shardingJdbc
 * 
 * @author lifeng
 * @date 2021年2月1日 下午2:38:40
 */
@SuppressWarnings("rawtypes")
public class ShardingFilter implements Filter {

	private Map<String, TableParser> parsers = Maps.newLRUCache(1000);
	private Field boundSqlfield;

	public ShardingFilter() {
		try {
			this.boundSqlfield = BoundSql.class.getField("sql");
			this.boundSqlfield.setAccessible(true);
		} catch (Exception e) {
		}
	}

	@Override
	public Object filter(Invocation invocation, FilterChain chain) throws Throwable {
		final Object[] queryArgs = invocation.getArgs();
		MappedStatement mappedStatement = (MappedStatement) queryArgs[0];
		final Object parameterObject = queryArgs[1];
		String mappedId = mappedStatement.getId();
		String namespace = StringUtils.substringBeforeLast(mappedId, ".");
		TableDefine define = TableDefines.lookup(namespace);
		if (define != null && this.needSharding(define)) {
			BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
			this.boundSqlfield.set(boundSql, this.doSharding(mappedId, define, boundSql));
		}
		return chain.filter(invocation);
	}

	private boolean needSharding(TableDefine define) {
		return StringUtils.isNotBlank(define.define.shardingAlgorithm()) || (define.define.shardingClass() != null
				&& ShardingStrategy.class.isAssignableFrom(define.define.shardingClass()));
	}

	private String doSharding(String mappedId, TableDefine define, BoundSql boundSql) {
		TableParser parser = this.getParser(mappedId, boundSql);
		List<TableParser.SqlToken> names = new ArrayList<>();
		parser.accept(names::add);
		String actualSql = parser.getSql();
		StringBuilder builder = new StringBuilder();
		int last = 0;
		for (TableParser.SqlToken name : names) {
			int start = name.getStart();
			if (start != last) {
				builder.append(actualSql, last, start);
				builder.append(this.doSharding(define, boundSql));
			}
			last = name.getEnd();
		}
		if (last != actualSql.length()) {
			builder.append(actualSql.substring(last));
		}
		return builder.toString();
	}

	private String doSharding(TableDefine define, BoundSql boundSql) {
		return ShardingStrategys.shardingTable(new SimpleShardingParam(define, boundSql));
	}

	private TableParser getParser(String mappedId, BoundSql boundSql) {
		TableParser parser = parsers.get(mappedId);
		if (parser == null) {
			return parsers.computeIfAbsent(mappedId, (key) -> {
				return new TableParser(boundSql.getSql());
			});
		}
		return parser;
	}

	public class SimpleShardingParam implements ShardingParam {
		private TableDefine table;
		private BoundSql boundSql;

		public SimpleShardingParam(TableDefine table, BoundSql boundSql) {
			this.table = table;
			this.boundSql = boundSql;
		}

		@Override
		public TableDefine getDefine() {
			return table;
		}

		@Override
		@SuppressWarnings("unchecked")
		public <U> U getFieldValue(String fieldName) {
			FieldMeta meta = this.table.meta.getFields().get(fieldName);
			if (meta == null || boundSql.getParameterObject() == null) {
				return null;
			}

			Object value = null;
			Object object = boundSql.getParameterObject();

			// QueryCondition
			if (object instanceof QueryCondition) {
				String column = StringUtils.convertProperty2Column(fieldName);
				value = ((QueryCondition) object).getEqualColumnValue(column);
			}

			// Simple Property
			else if (BeanUtils.isSimpleProperty(object.getClass())) {
				value = object;
			}

			// map
			else if (Map.class.isAssignableFrom(object.getClass())) {
				value = ((Map) object).get(fieldName);
			}

			// obj
			else {
				try {
					value = meta.getFieldValue(object);
				} catch (IllegalArgumentException | IllegalAccessException e) {
					e.printStackTrace();
				}
			}

			return (U) value;
		}
	}
}